/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.oron3.bouncer.genericgl;

import java.util.ArrayList;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11Ext;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.opengl.GLUtils;

/**
 * An OpenGL text label maker.
 *
 *
 * OpenGL labels are implemented by creating a Bitmap, drawing all the labels
 * into the Bitmap, converting the Bitmap into an Alpha texture, and drawing
 * portions of the texture using glDrawTexiOES.
 *
 * The benefits of this approach are that the labels are drawn using the high
 * quality anti-aliased font rasterizer, full character set support, and all the
 * text labels are stored on a single texture, which makes it faster to use.
 *
 * The drawbacks are that you can only have as many labels as will fit onto one
 * texture, and you have to recreate the whole texture if any label text
 * changes.
 *
 */
public class LabelMaker {
	/**
	 * Create a label maker
	 * or maximum compatibility with various OpenGL ES implementations,
	 * the strike width and height must be powers of two,
	 * We want the strike width to be at least as wide as the widest window.
	 *
	 * @param fullColor
	 *            true if we want a full color backing store (4444),
	 *            otherwise we generate a grey L8 backing store.
	 * @param strikeWidth
	 *            width of strike
	 * @param strikeHeight
	 *            height of strike
	 */
	public LabelMaker(final boolean fullColor, final int strikeWidth, final int strikeHeight) {
		mFullColor = fullColor;
		mStrikeWidth = strikeWidth;
		mStrikeHeight = strikeHeight;
		mTexelWidth = (float) (1.0 / mStrikeWidth);
		mTexelHeight = (float) (1.0 / mStrikeHeight);
		mClearPaint = new Paint();
		mClearPaint.setARGB(0, 0, 0, 0);
		mClearPaint.setStyle(Style.FILL);
		mState = STATE_NEW;
	}

	/**
	 * Call to initialize the class.
	 * Call whenever the surface has been created.
	 *
	 * @param gl
	 */
	public void initialize(final GL10 gl) {
		mState = STATE_INITIALIZED;
		final int[] textures = new int[1];
		gl.glGenTextures(1, textures, 0);
		mTextureID = textures[0];
		gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureID);

		// Use Nearest for performance.
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);

		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

		gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE);
	}

	/**
	 * Call when the surface has been destroyed
	 */
	public void shutdown(final GL10 gl) {
		if (gl != null) {
			if (mState > STATE_NEW) {
				final int[] textures = new int[1];
				textures[0] = mTextureID;
				gl.glDeleteTextures(1, textures, 0);
				mState = STATE_NEW;
			}
		}
	}

	/**
	 * Call before adding labels. Clears out any existing labels.
	 *
	 * @param gl
	 */
	public void beginAdding(final GL10 gl) {
		checkState(STATE_INITIALIZED, STATE_ADDING);
		mLabels.clear();
		mU = 0;
		mV = 0;
		mLineHeight = 0;
		final Bitmap.Config config = mFullColor ? Bitmap.Config.ARGB_4444 : Bitmap.Config.ALPHA_8;
		mBitmap = Bitmap.createBitmap(mStrikeWidth, mStrikeHeight, config);
		mCanvas = new Canvas(mBitmap);
		mBitmap.eraseColor(0);
	}

	/**
	 * Call to add a label
	 *
	 * @param gl
	 * @param text
	 *            the text of the label
	 * @param textPaint
	 *            the paint of the label
	 * @return the id of the label, used to measure and draw the label
	 */
	public int add(final GL10 gl, final String text, final Paint textPaint) {
		return add(gl, null, text, textPaint);
	}

	/**
	 * Call to add a label
	 *
	 * @param gl
	 * @param text
	 *            the text of the label
	 * @param textPaint
	 *            the paint of the label
	 * @return the id of the label, used to measure and draw the label
	 */
	public int add(final GL10 gl, final Drawable background, final String text, final Paint textPaint) {
		return add(gl, background, text, textPaint, 0, 0);
	}

	/**
	 * Call to add a label
	 * 
	 * @return the id of the label, used to measure and draw the label
	 */
	public int add(final GL10 gl, final Drawable drawable, final int minWidth, final int minHeight) {
		return add(gl, drawable, null, null, minWidth, minHeight);
	}

	/**
	 * Call to add a label
	 *
	 * @param gl
	 * @param text
	 *            the text of the label
	 * @param textPaint
	 *            the paint of the label
	 * @return the id of the label, used to measure and draw the label
	 */
	public int add(final GL10 gl, final Drawable background, final String text, final Paint textPaint, int minWidth, int minHeight) {
		checkState(STATE_ADDING, STATE_ADDING);
		final boolean drawBackground = background != null;
		final boolean drawText = (text != null) && (textPaint != null);

		final Rect padding = new Rect();
		if (drawBackground) {
			background.getPadding(padding);
			minWidth = Math.max(minWidth, background.getMinimumWidth());
			minHeight = Math.max(minHeight, background.getMinimumHeight());
		}

		int ascent = 0;
		int descent = 0;
		int measuredTextWidth = 0;
		if (drawText) {
			// Paint.ascent is negative, so negate it.
			ascent = (int) Math.ceil(-textPaint.ascent());
			descent = (int) Math.ceil(textPaint.descent());
			measuredTextWidth = (int) Math.ceil(textPaint.measureText(text));
		}
		final int textHeight = ascent + descent;
		final int textWidth = Math.min(mStrikeWidth, measuredTextWidth);

		final int padHeight = padding.top + padding.bottom;
		final int padWidth = padding.left + padding.right;
		final int height = Math.max(minHeight, textHeight + padHeight);
		int width = Math.max(minWidth, textWidth + padWidth);
		final int effectiveTextHeight = height - padHeight;
		final int effectiveTextWidth = width - padWidth;

		final int centerOffsetHeight = (effectiveTextHeight - textHeight) / 2;
		final int centerOffsetWidth = (effectiveTextWidth - textWidth) / 2;

		// Make changes to the local variables, only commit them
		// to the member variables after we've decided not to throw
		// any exceptions.

		int u = mU;
		int v = mV;
		int lineHeight = mLineHeight;

		if (width > mStrikeWidth) {
			width = mStrikeWidth;
		}

		// Is there room for this string on the current line?
		if (u + width > mStrikeWidth) {
			// No room, go to the next line:
			u = 0;
			v += lineHeight;
			lineHeight = 0;
		}
		lineHeight = Math.max(lineHeight, height);
		if (v + lineHeight > mStrikeHeight) { throw new IllegalArgumentException("Out of texture space."); }

		final int u2 = u + width;
		final int vBase = v + ascent;
		final int v2 = v + height;

		if (drawBackground) {
			background.setBounds(u, v, u + width, v + height);
			background.draw(mCanvas);
		}

		if (drawText) {
			mCanvas.drawText(text, u + padding.left + centerOffsetWidth, vBase + padding.top + centerOffsetHeight, textPaint);
		}

		// We know there's enough space, so update the member variables
		mU = u + width;
		mV = v;
		mLineHeight = lineHeight;
		mLabels.add(new Label(width, height, ascent, u, v + height, width, -height));
		return mLabels.size() - 1;
	}

	/**
	 * Call to end adding labels. Must be called before drawing starts.
	 *
	 * @param gl
	 */
	public void endAdding(final GL10 gl) {
		checkState(STATE_ADDING, STATE_INITIALIZED);
		gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureID);
		GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmap, 0);
		// Reclaim storage used by bitmap and canvas.
		mBitmap.recycle();
		mBitmap = null;
		mCanvas = null;
	}

	/**
	 * Get the width in pixels of a given label.
	 *
	 * @param labelID
	 * @return the width in pixels
	 */
	public float getWidth(final int labelID) {
		return mLabels.get(labelID).width;
	}

	/**
	 * Get the height in pixels of a given label.
	 *
	 * @param labelID
	 * @return the height in pixels
	 */
	public float getHeight(final int labelID) {
		return mLabels.get(labelID).height;
	}

	/**
	 * Get the baseline of a given label. That's how many pixels from the top of
	 * the label to the text baseline. (This is equivalent to the negative of
	 * the label's paint's ascent.)
	 *
	 * @param labelID
	 * @return the baseline in pixels.
	 */
	public float getBaseline(final int labelID) {
		return mLabels.get(labelID).baseline;
	}

	/**
	 * Begin drawing labels. Sets the OpenGL state for rapid drawing.
	 *
	 * @param gl
	 * @param viewWidth
	 * @param viewHeight
	 */
	public void beginDrawing(final GL10 gl, final float viewWidth, final float viewHeight) {
		checkState(STATE_INITIALIZED, STATE_DRAWING);
		gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureID);
		gl.glShadeModel(GL10.GL_FLAT);
		gl.glEnable(GL10.GL_BLEND);
		gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
		gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);
		gl.glMatrixMode(GL10.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		gl.glOrthof(0.0f, viewWidth, 0.0f, viewHeight, 0.0f, 1.0f);
		gl.glMatrixMode(GL10.GL_MODELVIEW);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		// Magic offsets to promote consistent rasterization.
		gl.glTranslatef(0.375f, 0.375f, 0.0f);
	}

	/**
	 * Draw a given label at a given x,y position, expressed in pixels, with the
	 * lower-left-hand-corner of the view being (0,0).
	 *
	 * @param gl
	 * @param x
	 * @param y
	 * @param labelID
	 */
	public void draw(final GL10 gl, final float x, final float y, final int labelID) {
		checkState(STATE_DRAWING, STATE_DRAWING);
		final Label label = mLabels.get(labelID);
		gl.glEnable(GL10.GL_TEXTURE_2D);
		((GL11) gl).glTexParameteriv(GL10.GL_TEXTURE_2D, GL11Ext.GL_TEXTURE_CROP_RECT_OES, label.mCrop, 0);
		((GL11Ext) gl).glDrawTexiOES((int) x, (int) y, 0, (int) label.width, (int) label.height);
	}

	/**
	 * Ends the drawing and restores the OpenGL state.
	 *
	 * @param gl
	 */
	public void endDrawing(final GL10 gl) {
		checkState(STATE_DRAWING, STATE_INITIALIZED);
		gl.glDisable(GL10.GL_TEXTURE_2D);
		gl.glDisable(GL10.GL_BLEND);
		gl.glMatrixMode(GL10.GL_PROJECTION);
		gl.glPopMatrix();
		gl.glMatrixMode(GL10.GL_MODELVIEW);
		gl.glPopMatrix();
	}

	private void checkState(final int oldState, final int newState) {
		if (mState != oldState) { throw new IllegalArgumentException("Can't call this method now."); }
		mState = newState;
	}

	private static class Label {
		public Label(final float width, final float height, final float baseLine, final int cropU, final int cropV, final int cropW, final int cropH) {
			this.width = width;
			this.height = height;
			baseline = baseLine;
			final int[] crop = new int[4];
			crop[0] = cropU;
			crop[1] = cropV;
			crop[2] = cropW;
			crop[3] = cropH;
			mCrop = crop;
		}

		public float width;
		public float height;
		public float baseline;
		public int[] mCrop;
	}

	private final int mStrikeWidth;
	private final int mStrikeHeight;
	private final boolean mFullColor;
	private Bitmap mBitmap;
	private Canvas mCanvas;
	private final Paint mClearPaint;

	private int mTextureID;

	private final float mTexelWidth;  // Convert texel to U
	private final float mTexelHeight; // Convert texel to V
	private int mU;
	private int mV;
	private int mLineHeight;
	private final ArrayList<Label> mLabels = new ArrayList<Label>();

	private static final int STATE_NEW = 0;
	private static final int STATE_INITIALIZED = 1;
	private static final int STATE_ADDING = 2;
	private static final int STATE_DRAWING = 3;
	private int mState;
}
